// This file is the kernel of CumquatGUI.
//
// Copyright (C) 2021, <liuxinouc@126.com>.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// CumquatGUI
// by Situ Design Studio
// Qingdao Shandong PRC, 2002-2021
//
// 司徒设计工作室 · 共和国山东青岛
//
const DEBUG = false

const CQGUI_TICK = 10 //millisecond

const MSG_MOUSE_DOWN = 1
const MSG_MOUSE_UP = 2
const MSG_MOUSE_MOVE = 3
const MSG_MOUSE_CLICK = 4
const MSG_MOUSE_WHEEL = 5
const MSG_TIMER = 6
const MSG_CLOSE_BOX = 7

// CQLayout::optType
const LAYOUT_TYPE_UNSPECIFIED = 0 // For example, Leaf node, no need to specify the type
const LAYOUT_TYPE_HZTL = 1        // Horizontal arrangement
const LAYOUT_TYPE_VERT = 2        // Vertical arrangement

// CQLayout::optStrategy
const LAYOUT_STG_UNSPECIFIED = 0         // For example, Root layout, no need to specify strategy
const LAYOUT_STG_KEEP_LEFT_OR_UP = 1     // Lean left or lean up
const LAYOUT_STG_KEEP_RIGHT_OR_DOWN = 2  // Lean right or lean down
const LAYOUT_STG_FILL_EMPTY = 3          // Take up the remaining space

// CQWnd::optMouseStatus
const WND_MOUSE_NORMAL = 1
const WND_MOUSE_HOVER = 2
const WND_MOUSE_PUSHDOWN = 3

// CQBox::optCloseBox
const BOX_CLOSE_OUTER_CLICK = 1
const BOX_CLOSE_LTBTN = 2
const BOX_CLOSE_RTBTN = 3
const BOX_CLOSE_PASSIVE = 4

// CQButton::optButtonType
const BUTTON_TYPE_NORMAL = 0
const BUTTON_TYPE_SPIN_ADD = 1
const BUTTON_TYPE_SPIN_SUB = 2

// CQList::optItemStyle
const LIST_ITEM_SINGEL_CHECK = 1
const LIST_ITEM_MULTI_CHECK = 2
const LIST_ITEM_CLICK_MSG = 3

// CQList::optItemAlign
const LIST_ITEM_LEFT = 1
const LIST_ITEM_CENTER = 2

// CQScolBar::optType
const SCOLBAR_TYPE_HZTL = 1
const SCOLBAR_TYPE_VERT = 2

// CQPrgsBar::optTitleAlign
const PRGSBAR_TITLE_CENTER = 1
const PRGSBAR_TITLE_UPSIDE = 2
const PRGSBAR_TITLE_BELOW = 3

// CQLabel::optType
const LABEL_TYPE_TEXT = 1
const LABEL_TYPE_IMAGE = 2

// CQLabel::optAlign
// When optType == LABEL_TYPE_TEXT
const LABEL_TXT_LEFT = 1
const LABEL_TXT_CENTER = 2
// When optType == LABEL_TYPE_IMAGE
const LABEL_IMG_CENTERED = 1
const LABEL_IMG_STRETCHED = 2

// Class and control definition
// CQDrawingArea::optThumbnailType
const OPT_DRA_NO_THUMBNAIL = 0
const OPT_DRA_THUMBNAIL_LEFT_TOP = 1
const OPT_DRA_THUMBNAIL_RIGHT_TOP = 2
const OPT_DRA_THUMBNAIL_LEFT_BOTTOM = 3
const OPT_DRA_THUMBNAIL_RIGHT_BOTTOM = 4
// CQStyle：The style manager
// CQLayout：Layout is the base class of the Window
// CQWnd：Window is the base class of DialogBox
// --CQScolBar：ScrollBar is an appendage of the Window
// CQBox：DialogBox is a container carrier for other controls
// CQButton：Normal Button
// CQCheck：Check Box
// CQSpin：Spin Button
// CQList：List Box
// CQCombo：Combo Box
// CQPrgsBar：Progress Bar
// CQLabel：Label, include Image Label and Text Label

class CQStyle{
    constructor(){
        CQStyle.SetDefaultStyle(this)
    }

    load(json_data){
        for(let key in json_data){
            this[key] = json_data[key]
        }
    }

    static SetDefaultStyle(obj){
        // Set the default style
        obj.font = '18px Microsoft YaHei'
        obj.crText = 'rgb(255, 255, 255)'            // Text color
        obj.numRoundCorner = 10                      // Radius of fillet
        obj.crFrameBorder = 'rgb(56, 93, 137)'       // Background-frame color
        obj.crLayoutBorder = 'rgb(255, 242, 0)'      // The color of the layout (for debugging)
        obj.numFrameBrdWidth = 2                     // Background-frame line width
        obj.crFrameFloor = 'rgb(79, 129, 188)'       // Fill color of background-frame
        obj.crUpperBorder = 'rgb(74, 172, 197)'      // Top frame color
        obj.numUpperBrdWidth = 1                     // Top frame line width
        obj.crUpperColorStop0 = 'rgb(228, 237, 255)' // Gradient color of the upper part of the top frame
        obj.crUpperColorStop1 = 'rgb(164, 196, 255)' // Gradient color at the bottom of the top frame
        obj.crPushDownColorStop0 = 'rgb(70, 55, 88)' // Gradient color on the upper part of the button pressed state
        obj.crPushDownColorStop1 = 'rgb(179, 162, 199)' // Gradient color at the bottom of the button pressed state
        obj.crCheckBorder = 'rgb(56, 93, 137)'       // The color of the checkmark frame
        obj.crCheckFloor = 'rgb(255, 255, 255)'      // The color of the checkmark background
        obj.crCheckMark = 'rgb(0, 0, 0)'             // The color of the selected checkmark
        obj.numCheckFrameWidth = 12                  // Side length of the checkmark
        obj.numCheckFrameBrdWidth = 2                // Line width of the checkmark
        obj.numCheckMarkWidth = 8                    // Side length of the selected checkmark
        obj.numCheckMarkOccupy = 20                  // The width occupied by the checkmark
        obj.numSpinBtnRatio = 0.3                    // Percentage of fine-tuning buttons
        obj.numPrgsTitleAlign = 2                    // Progress bar text position
        obj.numScolBarFrameHeight = 20               // Scroll bar basemap height
        obj.numScolBarBrdWidth = 3                   // Scroll bar button margin
        obj.numComboBtnMarkWidth = 16                // Combo box button mark side length
        obj.crListItemMouseHover = 'rgb(164, 196, 255)'// List item background color (mouse hover)
        obj.crListItemSelected = 'rgb(79, 129, 188)'   // List item background color (selected)
        obj.numListItemHeight = 30                   // List item height
        obj.numBoxTitleHeight = 30                   // Dialog title bar height
        obj.numBoxMarginWidth = 8                    // The width of the margin in the dialog box
        obj.numBoxLayoutType = 2                     // Dialog uses vertical layout
        obj.numBoxCloseStyle = 3                     // The dialog box uses the close button in the upper right corner
        obj.numAlphaOfRearBox = 0.6                  // Transparency of the Dialog Box in the background
    }
}

var globalStyle = new CQStyle

class Rect{
    constructor(x, y, w, h){
        this.x = x
        this.y = y
        this.w = w
        this.h = h
    }

    right(){
        return this.x + this.w
    }

    bottom(){
        return this.y + this.h
    }

    Clone(){
        return new Rect(this.x, this.y, this.w, this.h)
    }

    CutOff(rc){ // Cake cutting function
        let margin_left = rc.x - this.x
        let margin_right = this.right() - rc.right()
        let margin_top = rc.y - this.y
        let margin_bottom = this.bottom() - rc.bottom()

        if (   margin_left > margin_right
            && margin_left > margin_top
            && margin_left > margin_bottom){
            this.w = margin_left
        }
        else if (margin_right > margin_left
            && margin_right > margin_top
            && margin_right > margin_bottom){
            this.w = margin_right
            this.x = rc.right()
        }
        else if (margin_top > margin_left
            && margin_top > margin_right
            && margin_top > margin_bottom){
            this.h = margin_top
        }
        else {
            this.h = margin_bottom
            this.y = rc.bottom()
        }

        if (this.w < 0){this.w = 0}
        if (this.h < 0){this.h = 0}
    }

    Move(offset_x, offset_y){
        this.x += offset_x
        this.y += offset_y
    }

    Shrink(margin_left, margin_top, margin_right, margin_bottom){
        this.w -= (margin_left + margin_right)
        this.h -= (margin_top + margin_bottom)
        this.x += margin_left
        this.y += margin_top
        if (this.w < 0){this.w = 0}
        if (this.h < 0){this.h = 0}
    }

    PtInRect(point){
        return (point.x >= this.x
            && point.x <= this.right()
            && point.y >= this.y
            && point.y <= this.bottom())
    }

    GetCenterPoint(){
        return {x: this.x + Math.floor(this.w / 2),
                y: this.y + Math.floor(this.h / 2)}
    }

    IsCut(rc){
        return (Math.abs(this.x + this.right() - rc.x - rc.right()) < (this.w + rc.w) &&
                Math.abs(this.y + this.bottom() - rc.y - rc.bottom()) < (this.h + rc.h))
    }

    GetUnionRect(rc){
        let x = Math.min(this.x, rc.x)
        let y = Math.min(this.y, rc.y)
        let w = Math.max(this.right(), rc.right()) - x
        let h = Math.max(this.bottom(), rc.bottom()) - y
        return new Rect(x, y, w, h)
    }

    GetIntersectRect(rc){
        if (! this.IsCut(rc)){ return new Rect(-1, -1, -1, -1) }
        let l = Math.max(this.x, rc.x)
        let t = Math.max(this.y, rc.y)
        let r = Math.min(this.right(), rc.right())
        let b = Math.min(this.bottom(), rc.bottom())
        return new Rect(l, t, r-l, b-t)
    }
}

class CQLayout{ // In short, the Layout is only responsible for "occupying position"

    static numLayoutId = 0
    static numLayoutDebugColorR = 31
    static numLayoutDebugColorG = 0
    static numLayoutDebugColorB = 0
    static GetLayoutID(){
        CQLayout.numLayoutId++
        return CQLayout.numLayoutId
    }
    static GetDebugColor(){ // for debug
        if (CQLayout.numLayoutDebugColorR < 218 &&
            CQLayout.numLayoutDebugColorG == 0){
            CQLayout.numLayoutDebugColorR += 31
        }
        else if (CQLayout.numLayoutDebugColorG < 218 &&
            CQLayout.numLayoutDebugColorB == 0){
            CQLayout.numLayoutDebugColorG += 17
            CQLayout.numLayoutDebugColorR -= 17
        }
        else if (CQLayout.numLayoutDebugColorB < 218){
            CQLayout.numLayoutDebugColorB += 17
            CQLayout.numLayoutDebugColorG -= 17
        }
        else{
            CQLayout.numLayoutDebugColorR = 0
            CQLayout.numLayoutDebugColorG = 0
            CQLayout.numLayoutDebugColorB = 31
        }

        return ("rgb("+CQLayout.numLayoutDebugColorR
                  +","+CQLayout.numLayoutDebugColorG
                  +","+CQLayout.numLayoutDebugColorB
                  +")")
    }

    constructor(parent_layout, type, stg, apt_size){
        this.id = CQLayout.GetLayoutID()
        this.debugColor = CQLayout.GetDebugColor()
        
        this.optType = type
        this.optStrategy = stg
        this.txtAppointSize = "" + apt_size //Convert non-string to string

        this.rc = null
        this.rcEmpty = null
        if (parent_layout){
            this.Resize(parent_layout)
            parent_layout.AddChildLayout(this)
        }
    }

    AddChildLayout(new_layout){
        if (! this.lstChildren){
            this.lstChildren = new Array
        }
        this.lstChildren.push(new_layout)
    }

    SetBoundWnd(wnd){
        this.wndBound = wnd
    }

    GetTargetLayout(point){
        // Child node first
        if (this.lstChildren){
            for(let i = 0; i < this.lstChildren.length; i++){
                let target = this.lstChildren[i].GetTargetLayout(point)
                if (target){return target}
            }
        }
        if (this.rc.PtInRect(point)){return this}
        return null
    }

    Resize(parent_layout){
        if (DEBUG){
            if (parent_layout.rcEmpty.w <= 0 || parent_layout.rcEmpty.h <= 0){
                throw ("There is not enough space in the parent layout, parent_layout.id = " + parent_layout.id)
            }
        }

        if (this.optStrategy == LAYOUT_STG_FILL_EMPTY){
            // Take up the remaining space of the parent layout
            this.rc = parent_layout.rcEmpty.Clone()
        }
        else{
            switch(parent_layout.optType){
            case LAYOUT_TYPE_HZTL:  // Horizontal arrangement Layout

                let w = 0
                if (this.txtAppointSize.charAt(this.txtAppointSize.length-1) == '%'){
                    let ratio = parseInt(this.txtAppointSize)
                    w = Math.floor(parent_layout.rc.w * ratio / 100)
                }
                else{
                    w = parseInt(this.txtAppointSize)
                }

                switch(this.optStrategy){
                case LAYOUT_STG_KEEP_LEFT_OR_UP:
                    this.rc = new Rect(
                        parent_layout.rcEmpty.x,
                        parent_layout.rcEmpty.y,
                        w,
                        parent_layout.rcEmpty.h)
                    break

                case LAYOUT_STG_KEEP_RIGHT_OR_DOWN:
                    this.rc = new Rect(
                        parent_layout.rcEmpty.right() - w,
                        parent_layout.rcEmpty.y,
                        w,
                        parent_layout.rcEmpty.h)
                    break

                default:
                    throw "Unknown type optStrategy: " + parent_layout.optStrategy
                }
                break

            case LAYOUT_TYPE_VERT:  // Vertical arrangement Layout

                let h = 0
                if (this.txtAppointSize.charAt(this.txtAppointSize.length-1) == '%'){
                    let ratio = parseInt(this.txtAppointSize)
                    h = Math.floor(parent_layout.rc.h * ratio / 100)
                }
                else{
                    h = parseInt(this.txtAppointSize)
                }

                switch(this.optStrategy){
                case LAYOUT_STG_KEEP_LEFT_OR_UP:
                    this.rc = new Rect(
                        parent_layout.rcEmpty.x,
                        parent_layout.rcEmpty.y,
                        parent_layout.rcEmpty.w,
                        h)
                    break

                case LAYOUT_STG_KEEP_RIGHT_OR_DOWN:
                    this.rc = new Rect(
                        parent_layout.rcEmpty.x,
                        parent_layout.rcEmpty.bottom() - h,
                        parent_layout.rcEmpty.w,
                        h)
                    break

                default:
                    throw "Unknown type optStrategy: " + parent_layout.optStrategy
                }
                break

            default:
                throw "Unknown type optType: " + parent_layout.optType
            }
        }

        // Cut the cake on the parent layout
        parent_layout.rcEmpty.CutOff(this.rc)
        this.rcEmpty = this.rc.Clone()
        this._ResizeChildren()

        if (this.wndBound){
            this.wndBound.Resize(this.rc)
        }
    }

    _ResizeChildren(){ // Recalculate the position of each child window
        if (this.lstChildren){
            try{
                for(let i = 0; i < this.lstChildren.length; i++){
                    lstChildren[i].Resize(this)
                }
            }
            catch(err){
                if(DEBUG){
                    alert(err.description)
                    throw err.description
                }
            }
        }
    }

    DrawBoundWnd(ctx){
        if (this.wndBound){this.wndBound.Draw(ctx)}
        if (this.lstChildren){
            for(let i = 0; i < this.lstChildren.length; i++){
                this.lstChildren[i].DrawBoundWnd(ctx)
            }
        }
    }

    DrawLayout(ctx){
        ctx.fillStyle = this.debugColor
        ctx.fillRect(this.rc.x, this.rc.y, this.rc.w, this.rc.h)
        ctx.textBaseline = 'top'
        ctx.textAlign = 'left'
        ctx.fillStyle = "rgb(0, 0, 0)"
        ctx.fillText('Lay: ' + this.id, this.rc.x+2, this.rc.y+2)

        if (this.lstChildren){
            for(let i = 0; i < this.lstChildren.length; i++){
                this.lstChildren[i].DrawLayout(ctx)
            }
        }
    }
}

class CQWnd{
    constructor(cq, rc){
        this.cq = cq
        this.rc = rc
		this.r = globalStyle.numRoundCorner
        this.optMouseStatus = WND_MOUSE_NORMAL
        this.wndParent = null
        this.lstChildren = null
        this.isEnable = true
    }

    AddChild(wnd){
        if (! this.lstChildren){
            this.lstChildren = new Array
        }
        this.lstChildren.push(wnd)
        wnd.wndParent = this
    }

    CheckProgenitor(wnd){
        if (this.wndParent){
            if (this.wndParent == wnd){
                return true
            }
            else{
                return this.wndParent.CheckProgenitor(wnd)
            }
        }
        return false
    }

    PushPaintTask(wnd){
        this.cq.PushPaintTask(wnd)
    }

    Draw(ctx){
        // First, draw its own content
        this.DrawSelf(ctx)

        // Then, recursively call the drawing function of each child window
        if (this.lstChildren){
            for(let i = 0; i < this.lstChildren.length; i++){
                try{
                    this.lstChildren[i].Draw(ctx)
                }
                catch(err){
                    if(DEBUG){
                        alert(err.description)
                        throw err.description
                    }
                }
            }
        }
    }

    MsgProc(msg, param){

        // Child window is processed first
        if (this.lstChildren){
            for(let i = 0; i < this.lstChildren.length; i++){
                try{
                    let wnd = this.lstChildren[i]
                    if (wnd.MsgProc(msg, param)){
                        return true // Each message is processed only once
                    }
                }
                catch(err){
                    if(DEBUG){
                        alert(err.description)
                        throw err.description
                    }
                }
            }
        }

        // The last message processed in this window
        switch(msg){
        case MSG_TIMER:
            this.OnTimer(param)
            return true

        case MSG_MOUSE_DOWN:
            if (this.rc.PtInRect(param)){
                if (this.optMouseStatus != WND_MOUSE_PUSHDOWN){
                    this.optMouseStatus = WND_MOUSE_PUSHDOWN
                    this.MouseStatusChange()
                }
                return true
            }
            break

        case MSG_MOUSE_UP:
            if (this.rc.PtInRect(param)){
                if (this.optMouseStatus != WND_MOUSE_NORMAL){
                    this.optMouseStatus = WND_MOUSE_NORMAL
                    this.MouseStatusChange()
                }
                return true
            }
            break

        case MSG_MOUSE_MOVE:
            // The mouse movement message is an exception, it always return false
            if (this.rc.PtInRect(param)){
                this.cq.wndTrackMouse = this
                if (this.optMouseStatus == WND_MOUSE_NORMAL){
                    this.optMouseStatus = WND_MOUSE_HOVER
                    this.MouseStatusChange()
                }
            }
            else{
                if (this.cq.wndTrackMouse == this){
                    this.cq.wndTrackMouse = null
                }
                if (this.optMouseStatus != WND_MOUSE_NORMAL){
                    this.optMouseStatus = WND_MOUSE_NORMAL
                    this.MouseStatusChange()
                }
            }

        case MSG_MOUSE_WHEEL:
        case MSG_MOUSE_CLICK:
            break
        }

        return false
    }

    static DrawBaseFrame(ctx, rc, r){
        ctx.strokeStyle = globalStyle.crFrameBorder
        ctx.lineWidth = globalStyle.numFrameBrdWidth
        ctx.fillStyle = globalStyle.crFrameFloor
        CQWnd._DrawRoundCornerRect(ctx, rc, r)
    }

    static DrawRightRoundCornerBaseFrame(ctx, rc, r){
        ctx.strokeStyle = globalStyle.crFrameBorder
        ctx.lineWidth = globalStyle.numFrameBrdWidth
        ctx.fillStyle = globalStyle.crFrameFloor
        CQWnd._DrawRightRoundCornerRect(ctx, rc, r)
    }

    static DrawBottomRoundCornerBaseFrame(ctx, rc, r){
        ctx.strokeStyle = globalStyle.crFrameBorder
        ctx.lineWidth = globalStyle.numFrameBrdWidth
        ctx.fillStyle = globalStyle.crFrameFloor
        CQWnd._DrawBottomRoundCornerRect(ctx, rc, r)
    }

    static DrawUpperFrame(ctx, rc, r){
        ctx.strokeStyle = globalStyle.crUpperBorder
        ctx.lineWidth = globalStyle.numUpperBrdWidth
        let gdt = ctx.createLinearGradient(rc.x, rc.y, rc.x, rc.bottom())
        gdt.addColorStop(0, globalStyle.crUpperColorStop0)
        gdt.addColorStop(1, globalStyle.crUpperColorStop1)
        ctx.fillStyle = gdt
        CQWnd._DrawRoundCornerRect(ctx, rc, r)
    }

    static DrawPushDownFrame(ctx, rc, r){
        ctx.strokeStyle = globalStyle.crUpperBorder
        ctx.lineWidth = globalStyle.numUpperBrdWidth
        let gdt = ctx.createLinearGradient(rc.x, rc.y, rc.x, rc.bottom())
        gdt.addColorStop(0, globalStyle.crPushDownColorStop0)
        gdt.addColorStop(1, globalStyle.crPushDownColorStop1)
        ctx.fillStyle = gdt
        CQWnd._DrawRoundCornerRect(ctx, rc, r)
    }

    static _DrawRoundCornerRect(ctx, rc, r){
        if(r <= 0 || r > rc.w/2 || r > rc.h/2){
            ctx.fillRect(rc.x, rc.y, rc.w, rc.h)
            ctx.strokeRect(rc.x, rc.y, rc.w, rc.h)
        }
        else{
            ctx.beginPath()
            ctx.arc(rc.x+r, rc.y+r, r, Math.PI, 1.5*Math.PI)
            ctx.arc(rc.right()-r, rc.y+r, r, 1.5*Math.PI, 0)
            ctx.arc(rc.right()-r, rc.bottom()-r, r, 0, 0.5*Math.PI)
            ctx.arc(rc.x+r, rc.bottom()-r, r, 0.5*Math.PI, Math.PI)
            ctx.closePath()
            ctx.fill()
            ctx.stroke()
        }
    }

    static DrawTopRoundCornerRect(ctx, rc, r){
        if(r <= 0 || r > rc.w/2 || r > rc.h/2){
            ctx.fillRect(rc.x, rc.y, rc.w, rc.h)
            ctx.strokeRect(rc.x, rc.y, rc.w, rc.h)
        }
        else{
            ctx.beginPath()
            ctx.arc(rc.x+r, rc.y+r, r, Math.PI, 1.5*Math.PI)
            ctx.arc(rc.right()-r, rc.y+r, r, 1.5*Math.PI, 0)
            ctx.lineTo(rc.right(), rc.bottom())
            ctx.lineTo(rc.x, rc.bottom())
            ctx.lineTo(rc.x, rc.y+r)
            ctx.closePath()
            ctx.fill()
            ctx.stroke()
        }
    }

    static DrawBottomRoundCornerRect(ctx, rc, r){
        if(r <= 0 || r > rc.w/2 || r > rc.h/2){
            ctx.fillRect(rc.x, rc.y, rc.w, rc.h)
            ctx.strokeRect(rc.x, rc.y, rc.w, rc.h)
        }
        else{
            ctx.beginPath()
            ctx.moveTo(rc.x, rc.bottom()-r)
            ctx.lineTo(rc.x, rc.y)
            ctx.lineTo(rc.right(), rc.y)
            ctx.lineTo(rc.right(), rc.bottom()-r)   
            ctx.arc(rc.right()-r, rc.bottom()-r, r, 0, 0.5*Math.PI)       
            ctx.arc(rc.x+r, rc.bottom()-r, r, 0.5*Math.PI, Math.PI)
            ctx.closePath()
            ctx.fill()
            ctx.stroke()
        }
    }

    static _DrawRightRoundCornerRect(ctx, rc, r){
        if(r <= 0 || r > rc.w/2 || r > rc.h/2){
            ctx.fillRect(rc.x, rc.y, rc.w, rc.h)
            ctx.strokeRect(rc.x, rc.y, rc.w, rc.h)
        }
        else{
            ctx.beginPath()
            ctx.arc(rc.right()-r, rc.y+r, r, 1.5*Math.PI, 0)
            ctx.arc(rc.right()-r, rc.bottom()-r, r, 0, 0.5*Math.PI)
            ctx.lineTo(rc.x, rc.bottom())
            ctx.lineTo(rc.x, rc.y)
            ctx.lineTo(rc.right()-r, rc.y)
            ctx.closePath()
            ctx.fill()
            ctx.stroke()
        }
    }

    static DrawCheckMark(ctx, rc, checked){
        ctx.strokeStyle = globalStyle.crCheckBorder
        ctx.lineWidth = globalStyle.numCheckFrameBrdWidth
        ctx.fillStyle = globalStyle.crCheckFloor
        let center = rc.GetCenterPoint()
        let frm_w = globalStyle.numCheckFrameWidth
        let mrk_w = globalStyle.numCheckMarkWidth
        let frm_half = Math.floor(frm_w / 2)
        let mrk_half = Math.floor(mrk_w / 2)
        ctx.fillRect(center.x - frm_half, center.y - frm_half, frm_w, frm_w)
        ctx.strokeRect(center.x - frm_half, center.y - frm_half, frm_w, frm_w)

        if (checked){
            ctx.fillStyle = globalStyle.crCheckMark
            ctx.fillRect(center.x - mrk_half, center.y - mrk_half, mrk_w, mrk_w)
        }
    }

    static DrawPopButton(ctx, rc){
        ctx.strokeStyle = globalStyle.crCheckBorder
        ctx.lineWidth = globalStyle.numCheckFrameBrdWidth
        ctx.fillStyle = globalStyle.crCheckFloor
        let center = rc.GetCenterPoint()
        let frm_w = globalStyle.numComboBtnMarkWidth
        let mrk_w = globalStyle.numCheckMarkWidth
        let frm_half = Math.floor(frm_w / 2)
        let mrk_half = Math.floor(mrk_w / 2)
        ctx.fillRect(center.x - frm_half, center.y - frm_half, frm_w, frm_w)
        ctx.strokeRect(center.x - frm_half, center.y - frm_half, frm_w, frm_w)
        ctx.fillStyle = globalStyle.crCheckMark
        ctx.beginPath()
        ctx.moveTo(center.x - mrk_half, center.y - mrk_half)
        ctx.lineTo(center.x + mrk_half, center.y - mrk_half)
        ctx.moveTo(center.x - mrk_half, center.y)
        ctx.lineTo(center.x + mrk_half, center.y)
        ctx.moveTo(center.x - mrk_half, center.y + mrk_half)
        ctx.lineTo(center.x + mrk_half, center.y + mrk_half)
        ctx.stroke()
    }

    DrawButtonBase(ctx){
        switch (this.optMouseStatus){
        case WND_MOUSE_NORMAL:
            CQWnd.DrawBaseFrame(ctx, this.rc, this.r)
            break
        case WND_MOUSE_HOVER:
            CQWnd.DrawUpperFrame(ctx, this.rc, this.r)
            break
        case WND_MOUSE_PUSHDOWN:
            CQWnd.DrawPushDownFrame(ctx, this.rc, this.r)
            break
        }
    }

    static DrawTextCenter(ctx, rc, txt, color = globalStyle.crText){
        let center_x = rc.x + rc.w / 2
        let center_y = rc.y + rc.h / 2
        ctx.textBaseline = 'middle'
        ctx.textAlign = 'center'
        ctx.fillStyle = color
        ctx.fillText(txt, center_x, center_y)
    }

    DrawFrame(ctx, border_w, color){
        ctx.strokeStyle = color
        ctx.lineWidth = border_w
        ctx.strokeRect(this.rc.x, this.rc.y, this.rc.w, this.rc.h)
    }

    DrawImage(ctx, x, y, w, h, img, sx, sy){
        // Note: The x and y parameters are relative coordinates relative to this control
        // This drawing function guarantees that the picture will not be drawn outside the rc area

        if (x > this.rc.w || y > this.rc.h) return

        if (x < 0){
            w += x // Note that x is negative
            x = 0
        }
        if (y < 0){
            h += y // Note that y is negative
            y = 0
        }
        if ((x + w) > this.rc.w){
            w = this.rc.w - x
        }
        if ((y + h) > this.rc.h){
            h = this.rc.h - y
        }

        ctx.drawImage(img, sx, sy, w, h, this.rc.x+x, this.rc.y+y, w, h)
    }

    MouseStatusChange(){} // Override this function

    DrawSelf(ctx){} // Override this function

    OnTimer(tiner_id){} // Override this function
}

class CQBox extends CQWnd{
    constructor(cq, rc, title, option_close_box){
        super(cq, rc)
        this.title = title

        let rc_in = rc.Clone()
        rc_in.Shrink(
            globalStyle.numBoxMarginWidth,
            globalStyle.numBoxTitleHeight + globalStyle.numBoxMarginWidth,
            globalStyle.numBoxMarginWidth,
            globalStyle.numBoxMarginWidth)
        this.rcInner = rc_in

        this.rcTitleBar = new Rect(rc.x, rc.y, rc.w, globalStyle.numBoxTitleHeight)
        let layoutTitleBar = new CQLayout(null, LAYOUT_TYPE_HZTL, LAYOUT_STG_UNSPECIFIED, 0)
        layoutTitleBar.rc = this.rcTitleBar.Clone()
        layoutTitleBar.rcEmpty = layoutTitleBar.rc.Clone()
        this.title_layout = layoutTitleBar

        let layout = new CQLayout(null, globalStyle.numBoxLayoutType, LAYOUT_STG_UNSPECIFIED, 0)
        layout.rc = this.rcInner.Clone()
        layout.rcEmpty = layout.rc.Clone()
        this.inner_layout = layout

        this.optCloseBox = option_close_box

        // Create a close button
        if (this.optCloseBox == BOX_CLOSE_LTBTN){
            let layout_close_button = this.AddLayout(this.title_layout,
                LAYOUT_TYPE_UNSPECIFIED, LAYOUT_STG_KEEP_LEFT_OR_UP,
                globalStyle.numBoxTitleHeight)
            let close_btn = new CQButton(cq, layout_close_button, "x", globalStyle.crCheckMark)
            close_btn.OnClick = function(){
                this.wndParent.DoCloseBox()
            }
            this.AddChild(close_btn)
        }
        else if (this.optCloseBox == BOX_CLOSE_RTBTN){
            let layout_close_button = this.AddLayout(this.title_layout,
                LAYOUT_TYPE_UNSPECIFIED, LAYOUT_STG_KEEP_RIGHT_OR_DOWN,
                globalStyle.numBoxTitleHeight)
            let close_btn = new CQButton(cq, layout_close_button, "x", globalStyle.crCheckMark)
            close_btn.OnClick = function(){
                this.wndParent.DoCloseBox()
            }
            this.AddChild(close_btn)
        }
    }

    MsgProc(msg, param){
        if (super.MsgProc(msg, param)){return true}

        switch (msg){
        case MSG_MOUSE_DOWN:
            if (this.optCloseBox == BOX_CLOSE_OUTER_CLICK){
                if (! this.rc.PtInRect(param)){
                    this.DoCloseBox()
                    return true
                }
            }
            break
            
        case MSG_CLOSE_BOX:
            this.DoCloseBox()
            return true
            break
        }

        return false
    }

    DrawSelf(ctx){
        CQWnd.DrawBaseFrame(ctx, this.rc, this.r)

        // Center text
        let rc_title = this.rc.Clone()
        rc_title.h = globalStyle.numBoxTitleHeight
        if (this.optCloseBox == BOX_CLOSE_LTBTN){
            rc_title.x += globalStyle.numBoxTitleHeight
            rc_title.w -= globalStyle.numBoxTitleHeight
        }
        CQWnd.DrawTextCenter(ctx, rc_title, this.title)
    }

    GetInnerLayout(){
        return this.inner_layout
    }

    AddLayout(layout, type, stg, apt_sz){
        return new CQLayout(layout, type, stg, apt_sz)
    }

    AddButton(layout, title){
        let btn = new CQButton(this.cq, layout, title)
        this.AddChild(btn)
        return btn
    }

    AddCheck(layout, title, checked){
        let chk = new CQCheck(this.cq, layout, title, checked)
        this.AddChild(chk)
        return chk
    }

    AddSpin(layout, value, digit, step){
        let spn = new CQSpin(this.cq, layout, value, digit, step)
        this.AddChild(spn)
        return spn
    }

    AddList(layout, item_style, item_list){
        let lst = new CQList(this.cq, layout, item_style, item_list)
        this.AddChild(lst)
        return lst
    }

    AddCombo(layout, item_list, cur_sel, list_cap){
        let cbo = new CQCombo(this.cq, layout, item_list, cur_sel, list_cap)
        this.AddChild(cbo)
        return cbo
    }

    AddPrgsBar(layout, format, percentage){
        let pgs = new CQPrgsBar(this.cq, layout, format, percentage)
        this.AddChild(pgs)
        return pgs
    }

    AddTxtLabel(layout, txt, align, color){
        let lbl = new CQLabel(this.cq, layout, LABEL_TYPE_TEXT, txt, align, color)
        this.AddChild(lbl)
        return lbl
    }

    AddImgLabel(layout, img, align, size){
        let lbl = new CQLabel(this.cq, layout, LABEL_TYPE_IMAGE, img, align, size)
        this.AddChild(lbl)
        return lbl
    }

    DoCloseBox(){
        this.cq.CloseIndependentBox(this) // This function will call OnCloseBox()
    }

    OnCloseBox(){} // Override this function
}

class CQScolBar extends CQWnd{
    constructor(wndParent, type){
        let rc = new Rect(0, 0, 0, 0)
        super(wndParent.cq, rc)
        this.wndParent = wndParent
        this.optType = type

        // Calculate the size of the Scroll Bar
        switch (this.optType){
        case SCOLBAR_TYPE_HZTL:
            this.rc = new Rect(
                wndParent.rc.x,
                wndParent.rc.bottom() - globalStyle.numScolBarFrameHeight,
                wndParent.rc.h,
                globalStyle.numScolBarFrameHeight)
            break

        case SCOLBAR_TYPE_VERT:
            this.rc = new Rect(
                wndParent.rc.right() - globalStyle.numScolBarFrameHeight,
                wndParent.rc.y,
                globalStyle.numScolBarFrameHeight,
                wndParent.rc.h)
            break
        }

        let brd = globalStyle.numScolBarBrdWidth
        this.rcUpperArea = this.rc.Clone()
        this.rcUpperArea.Shrink(brd, brd, brd, brd)
        this.r = Math.floor(globalStyle.numScolBarFrameHeight / 2)
        this.r_upper = this.r - brd

        // Related variables
        this.numRange = 1 // This value cannot be 0, the minimum is 1
        this.numPageSize = 0
        this.numPos = 0
        // For example:
        // A list box has 40 list items, 6 items can be displayed on page, and the top item is currently the 25th item, then:
        // numRange = 40;    numPageSize = 6;    numPos = 24;
        this.isMouseDrag = false
        this.numMouseOldPos = 0
        this.ptMouseOldPoint = {x: 0, y: 0} // Record the coordinates when the mouse clicks on the button
            // (The vertical scroll bar records the y coordinate, the horizontal scroll bar records the x coordinate)
    }

    MsgProc(msg, param){
        if (msg == MSG_MOUSE_WHEEL){
            return false
        }

        let rc_upper_btn = this.GetUpperBtnRect()
        switch (msg){
        case MSG_MOUSE_DOWN:
            if (rc_upper_btn.PtInRect(param)){
                this.isMouseDrag = true
                this.numMouseOldPos = this.numPos
                this.ptMouseOldPoint = {x: param.x, y: param.y}
                this.cq.wndTrackMouse == this
                return true
            }
            break

        case MSG_MOUSE_UP:
            this.isMouseDrag = false
            if (this.cq.wndTrackMouse == this){
                this.cq.wndTrackMouse = null
            }
            return true

        case MSG_MOUSE_MOVE:
            if (this.isMouseDrag){
                let dMove = 0
                let dw = 0
                switch (this.optType){
                case SCOLBAR_TYPE_HZTL:
                    dMove = param.x - this.ptMouseOldPoint.x
                    dw = this.rcUpperArea.w - rc_upper_btn.w
                    break
                case SCOLBAR_TYPE_VERT:
                    dMove = param.y - this.ptMouseOldPoint.y
                    dw = this.rcUpperArea.h - rc_upper_btn.h
                    break
                }
                let posNew = this.numMouseOldPos + Math.floor(
                    dMove * (this.numRange - this.numPageSize - 1) / dw)
                this.DoSetPos(posNew)
                return true
            }
            break

        case MSG_MOUSE_CLICK:
            if (this.rc.PtInRect(param)){
                switch (this.optType){
                case SCOLBAR_TYPE_HZTL:
                    if (param.x < rc_upper_btn.x){
                        this.DoPageUp()
                    }
                    else if (param.x > rc_upper_btn.right()){
                        this.DoPageDown()
                    }
                    break
                case SCOLBAR_TYPE_VERT:
                    if (param.y < rc_upper_btn.y){
                        this.DoPageUp()
                    }
                    else if (param.y > rc_upper_btn.bottom()){
                        this.DoPageDown()
                    }
                    break
                }
                return true
            }
            break
        }
        return false
    }

    DrawSelf(ctx){
        if (this.numPageSize == this.numRange){
            return // No need to display the scroll bar
        }
        switch (this.optType){
        case SCOLBAR_TYPE_HZTL:
            CQWnd.DrawBottomRoundCornerBaseFrame(ctx, this.rc, this.r)
            break
        case SCOLBAR_TYPE_VERT:
            CQWnd.DrawRightRoundCornerBaseFrame(ctx, this.rc, this.r)
            break
        }

        let rc_upper_btn = this.GetUpperBtnRect()
        if (rc_upper_btn){CQWnd.DrawUpperFrame(ctx, rc_upper_btn, this.r_upper)}
    }

    GetUpperBtnRect(){
        switch (this.optType){
        case SCOLBAR_TYPE_HZTL:
            let btn_w = Math.floor(this.rcUpperArea.w * this.numPageSize / this.numRange)
            if (btn_w < this.rcUpperArea.h){// The smallest button is also square, it can’t be smaller
                btn_w = this.rcUpperArea.h
            }
            let btn_left = this.rcUpperArea.x + Math.floor(
                (this.rcUpperArea.w - btn_w) * this.numPos / (this.numRange - this.numPageSize))
            return new Rect(btn_left, this.rcUpperArea.y, btn_w, this.rcUpperArea.h)

        case SCOLBAR_TYPE_VERT:
            let btn_h = Math.floor(this.rcUpperArea.h * this.numPageSize / this.numRange)
            if (btn_h < this.rcUpperArea.w){// The smallest button is also square, it can’t be smaller
                btn_h = this.rcUpperArea.w
            }
            let btn_top = this.rcUpperArea.y + Math.floor(
                (this.rcUpperArea.h - btn_h) * this.numPos / (this.numRange - this.numPageSize))
            return new Rect(this.rcUpperArea.x, btn_top, this.rcUpperArea.w, btn_h)
        }
        return null
    }

    DoPageUp(){
        if (this.numPos == 0){return}
        this.numPos -= (this.numPageSize - 1)
        if (this.numPos < 0){this.numPos = 0}
        this.OnChangePos(this.numPos)
    }

    DoPageDown(){
        if (this.numPos >= this.numRange){return}
        this.numPos += (this.numPageSize - 1)
        if (this.numPos > (this.numRange - this.numPageSize)){
            this.numPos = this.numRange - this.numPageSize
        }
        this.OnChangePos(this.numPos)
    }

    DoStepUp(){
        if (this.numPos > 0){
            this.numPos--
            this.OnChangePos(this.numPos)
        }
    }

    DoStepDown(){
        if (this.numPos < this.numRange - this.numPageSize){
            this.numPos++
            this.OnChangePos(this.numPos)
        }
    }

    DoSetRange(range){
        if (range > 1){	// Less than 1 will cause a division by 0 error
            this.numRange = range
            this.DoSetPageSize (this.numPageSize)
            this.DoSetPos (this.numPos)
        }
    }

    DoSetPageSize(size){
        this.numPageSize = size
        if (this.numPageSize > this.numRange){
            this.numPageSize = this.numRange
        }
    }

    DoSetPos(pos){
        if (this.numPos == pos){return}
        this.numPos = pos
        if (this.numPos <= 0){this.numPos = 0}
        else if (this.numPos > (this.numRange - this.numPageSize)){
            this.numPos = this.numRange - this.numPageSize
        }
        this.OnChangePos(this.numPos)
    }

    OnChangePos(pos){// Overload this function in the User Custom Box
        if (this.wndParent && this.wndParent.DoSetTopIndex){
            this.wndParent.DoSetTopIndex(pos)
        }
    }
}

class CQButton extends CQWnd{
    constructor(cq, layout, title, cr_title = globalStyle.crText){
        super(cq, layout.rc)
        layout.SetBoundWnd(this)
        this.r = globalStyle.numRoundCorner
        this.title = title
        this.crTitle = cr_title
        this.optButtonType = BUTTON_TYPE_NORMAL
    }

    DrawSelf(ctx){
        // Draw the background of buttons in different states
        switch (this.optButtonType){
        case BUTTON_TYPE_NORMAL:
            this.DrawButtonBase(ctx)
            break
        case BUTTON_TYPE_SPIN_ADD:
            this.DrawSpinAddButton(ctx)
            break
        case BUTTON_TYPE_SPIN_SUB:
            this.DrawSpinSubButton(ctx)
            break
        }

        CQWnd.DrawTextCenter(ctx, this.rc, this.title, this.crTitle)
    }

    DrawSpinAddButton(ctx){
        let rc = new Rect(this.rc.x, this.rc.y, this.rc.w, this.rc.h * 2)
        let gdt = ctx.createLinearGradient(rc.x, rc.y, rc.x, rc.bottom())
        switch (this.optMouseStatus){
        case WND_MOUSE_NORMAL:
            ctx.strokeStyle = globalStyle.crFrameBorder
            ctx.lineWidth = globalStyle.numFrameBrdWidth
            ctx.fillStyle = globalStyle.crFrameFloor
            break
        case WND_MOUSE_HOVER:
            ctx.strokeStyle = globalStyle.crUpperBorder
            ctx.lineWidth = globalStyle.numUpperBrdWidth
            gdt.addColorStop(0, globalStyle.crUpperColorStop0)
            gdt.addColorStop(1, globalStyle.crUpperColorStop1)
            ctx.fillStyle = gdt
            break
        case WND_MOUSE_PUSHDOWN:
            ctx.strokeStyle = globalStyle.crUpperBorder
            ctx.lineWidth = globalStyle.numUpperBrdWidth
            gdt.addColorStop(0, globalStyle.crPushDownColorStop0)
            gdt.addColorStop(1, globalStyle.crPushDownColorStop1)
            ctx.fillStyle = gdt
            break
        }
        CQWnd.DrawTopRoundCornerRect(ctx, this.rc, this.r)
    }

    DrawSpinSubButton(ctx){
        let rc = new Rect(this.rc.x, this.rc.y - this.rc.h, this.rc.w, this.rc.h * 2)
        let gdt = ctx.createLinearGradient(rc.x, rc.y, rc.x, rc.bottom())
        switch (this.optMouseStatus){
        case WND_MOUSE_NORMAL:
            ctx.strokeStyle = globalStyle.crFrameBorder
            ctx.lineWidth = globalStyle.numFrameBrdWidth
            ctx.fillStyle = globalStyle.crFrameFloor
            break
        case WND_MOUSE_HOVER:
            ctx.strokeStyle = globalStyle.crUpperBorder
            ctx.lineWidth = globalStyle.numUpperBrdWidth
            gdt.addColorStop(0, globalStyle.crUpperColorStop0)
            gdt.addColorStop(1, globalStyle.crUpperColorStop1)
            ctx.fillStyle = gdt
            break
        case WND_MOUSE_PUSHDOWN:
            ctx.strokeStyle = globalStyle.crUpperBorder
            ctx.lineWidth = globalStyle.numUpperBrdWidth
            gdt.addColorStop(0, globalStyle.crPushDownColorStop0)
            gdt.addColorStop(1, globalStyle.crPushDownColorStop1)
            ctx.fillStyle = gdt
            break
        }
        CQWnd.DrawBottomRoundCornerRect(ctx, this.rc, this.r)
    }

    MsgProc(msg, param){
        if (super.MsgProc(msg, param)){return true}

        switch (msg){
        case MSG_MOUSE_CLICK:
            if (this.rc.PtInRect(param)){
                this.OnClick()
                return true
            }
        }

        return false
    }

    DoClick(){
        this.OnClick()
    }

    MouseStatusChange(){
        this.PushPaintTask(this)
    }

    OnPushDown(){} // Overload this function

    OnPopUp(){} // Overload this function

    OnClick(){} // Overload this function
}

class CQCheck extends CQWnd{
    constructor(cq, layout, title, check){
        super(cq, layout.rc)
        layout.SetBoundWnd(this)
        this.r = globalStyle.numRoundCorner
        this.title = title
        this.isChecked = check
    }

    DrawSelf(ctx){
        this.DrawButtonBase(ctx)

        let rc_mark = new Rect(this.rc.x + this.r,
            this.rc.y,
            globalStyle.numCheckMarkOccupy,
            this.rc.h)
        let rc_text = new Rect(
            this.rc.x + this.r + globalStyle.numCheckMarkOccupy,
            this.rc.y,
            this.rc.w - (this.r * 2) - globalStyle.numCheckMarkOccupy,
            this.rc.h)
        CQWnd.DrawCheckMark(ctx, rc_mark, this.isChecked)
        CQWnd.DrawTextCenter(ctx, rc_text, this.title)
    }

    MsgProc(msg, param){
        if (super.MsgProc(msg, param)){return true}

        switch (msg){
        case MSG_MOUSE_CLICK:
            if (this.rc.PtInRect(param)){
                this.OnClick()
                return true
            }
        }

        return false
    }

    DoFlipCheck(){
        if (this.isChecked){
            this.DoUncheck()
        }
        else{
            this.DoCheck()
        }
    }

    DoCheck(){
        if (! this.isChecked){
            this.isChecked = true
            this.PushPaintTask(this)
            this.OnCheckChange()
        }
    }

    DoUncheck(){
        if (this.isChecked){
            this.isChecked = false
            this.PushPaintTask(this)
            this.OnCheckChange()
        }
    }

    MouseStatusChange(){
        this.PushPaintTask(this)
    }

    OnClick(){} // Overload this function, generally you can call DoFlipCheck() directly

    OnCheckChange(){} // Overload this function
}

class CQSpin extends CQWnd{
    constructor(cq, layout, value, digit, step){
        super(cq, layout.rc)
        layout.SetBoundWnd(this)
        this.r = globalStyle.numRoundCorner
        this.numValue = value
        this.numFixDigit = digit // The number of digits reserved for decimals, 0 means integer
        this.numStep = step

        let inner_layout = new CQLayout(null, LAYOUT_TYPE_HZTL, LAYOUT_STG_UNSPECIFIED, 0)
        inner_layout.rc = this.rc.Clone()
        inner_layout.rcEmpty = this.rc.Clone()
        this.inner_layout = inner_layout

        let right_layout = new CQLayout(this.inner_layout, LAYOUT_TYPE_VERT,
            LAYOUT_STG_KEEP_RIGHT_OR_DOWN, "" + globalStyle.numSpinBtnRatio * 100 + "%")
        let layout_add_btn = new CQLayout(right_layout, LAYOUT_TYPE_UNSPECIFIED,
            LAYOUT_STG_KEEP_LEFT_OR_UP, "50%")
        let layout_sub_btn = new CQLayout(right_layout, LAYOUT_TYPE_UNSPECIFIED,
            LAYOUT_STG_FILL_EMPTY)
        
        let wndAddButton = new CQButton(this.cq, layout_add_btn, "+", globalStyle.crCheckMark)
        wndAddButton.optButtonType = BUTTON_TYPE_SPIN_ADD
        wndAddButton.OnClick = function(){
            this.wndParent.DoClickAddBtn()
        }

        let wndSubButton = new CQButton(this.cq, layout_sub_btn, "-", globalStyle.crCheckMark)
        wndSubButton.optButtonType = BUTTON_TYPE_SPIN_SUB
        wndSubButton.OnClick = function(){
            this.wndParent.DoClickSubBtn()
        }

        this.AddChild(wndAddButton)
        this.AddChild(wndSubButton)
    }

    MsgProc(msg, param){
        super.MsgProc(msg, param)
    }

    DrawSelf(ctx){
        CQWnd.DrawBaseFrame(ctx, this.rc, this.r)

        let rc_text = new Rect(
            this.rc.x,
            this.rc.y,
            Math.floor(this.rc.w * (1 - globalStyle.numSpinBtnRatio)),
            this.rc.h)
        CQWnd.DrawTextCenter(ctx, rc_text, "" + this.numValue)
    }

    _FixValue(){
        if (this.numFixDigit){
            this.numValue = parseFloat(this.numValue.toFixed(this.numFixDigit))
        }
        else{
            this.numValue = parseInt(this.numValue)
        }
    }

    DoClickAddBtn(){
        this.numValue += this.numStep
        this._FixValue()
        this.PushPaintTask(this)
        this.OnValueChange(this.numValue)
    }

    DoClickSubBtn(){
        this.numValue -= this.numStep
        this._FixValue()
        this.PushPaintTask(this)
        this.OnValueChange(this.numValue)
    }

    OnValueChange(val){} // Overload this function

    GetValue(){
        return this.numValue
    }
}

class CQList extends CQWnd{
    constructor(cq, layout, item_style, item_list){
        super(cq, layout.rc)
        layout.SetBoundWnd(this)
        this.r = 0

        this.optItemStyle = item_style
        this.optItemAlign = LIST_ITEM_LEFT
        this.lstSelected = new Array
        this.lstContent = item_list.slice(0)

        this.numTopIndex = 0
        this.numCanDisplayed = Math.floor(this.rc.h / globalStyle.numListItemHeight)
        this.rc.h = this.numCanDisplayed * globalStyle.numListItemHeight
                    + globalStyle.numFrameBrdWidth * 2
        this.numMouseHoverItem = -1

        if (this.lstContent.length > this.numCanDisplayed){
            this.v_scolbar = new CQScolBar(this, SCOLBAR_TYPE_VERT)
            this.v_scolbar.DoSetRange(this.lstContent.length)
            this.v_scolbar.DoSetPageSize(this.numCanDisplayed)
            this.v_scolbar.DoSetPos(0)
            this.rc.w -= globalStyle.numScolBarFrameHeight
        }
        this.inner_layout = new CQLayout(layout, LAYOUT_TYPE_VERT, LAYOUT_STG_FILL_EMPTY, 0)
    }

    MsgProc(msg, param){
        super.MsgProc(msg, param)

        // The scroll bar handles messages first
        if (this.v_scolbar){
            if (this.v_scolbar.MsgProc(msg, param)){return true}
        }

        switch (msg){
        case MSG_MOUSE_MOVE:
            let old_hover = this.numMouseHoverItem
            if (this.rc.PtInRect(param)){
                this.numMouseHoverItem = Math.floor(
                    (param.y - this.rc.y - globalStyle.numFrameBrdWidth) / globalStyle.numListItemHeight)
                if (this.numMouseHoverItem != old_hover){
                    this.PushPaintTask(this)
                }
                return true
            }
            else{
                this.numMouseHoverItem = -1
                this.PushPaintTask(this)
            }
            break

        case MSG_MOUSE_WHEEL:
            if (param.deltaY < 0){
                if (this.numTopIndex > 0){
                    this.numTopIndex--
                    if (this.v_scolbar){
                        this.v_scolbar.DoSetPos(this.numTopIndex)
                    }
                    this.PushPaintTask(this)
                }
            }
            else {
                if (this.numTopIndex + this.numCanDisplayed < this.lstContent.length){
                    this.numTopIndex++
                    if (this.v_scolbar){
                        this.v_scolbar.DoSetPos(this.numTopIndex)
                    }
                    this.PushPaintTask(this)
                }
            }
            return true

        case MSG_MOUSE_CLICK:
            this.numMouseHoverItem = Math.floor(
                (param.y - this.rc.y - globalStyle.numFrameBrdWidth) / globalStyle.numListItemHeight)
            let click_index = this.numTopIndex + this.numMouseHoverItem
            if (click_index >= 0 && click_index < this.lstContent.length){
                switch (this.optItemStyle){
                case LIST_ITEM_SINGEL_CHECK:
                case LIST_ITEM_MULTI_CHECK:
                    if (this.IsSelected(click_index)){
                        this.DoUnselect(click_index)
                    }
                    else{
                        this.DoSelect(click_index)
                    }
                    break
                case LIST_ITEM_CLICK_MSG:
                    this.OnClickItem(click_index)
                }
            }
            return true
        }

        return false
    }

    SetItemAlign(align){
        if (this.optItemAlign != align){
            this.optItemAlign = align
            this.PushPaintTask(this)
        }
    }

    DrawSelf(ctx){
        for (let i = 0; i < this.numCanDisplayed; i++){
            let index = this.numTopIndex + i
            if (index > this.lstContent.length - 1){break}

            let rc_item = new Rect(
                this.rc.x + globalStyle.numFrameBrdWidth,
                this.rc.y + globalStyle.numFrameBrdWidth + globalStyle.numListItemHeight * i,
                this.rc.w - globalStyle.numFrameBrdWidth * 2,
                globalStyle.numListItemHeight)

            // Draw item background
            if (this.numMouseHoverItem == i){
                ctx.fillStyle = globalStyle.crListItemMouseHover
            }
            else if(this.IsSelected(index)){
                ctx.fillStyle = globalStyle.crListItemSelected
            }
            else{
                ctx.fillStyle = globalStyle.crFrameFloor
            }
            ctx.fillRect(rc_item.x, rc_item.y, rc_item.w, rc_item.h)

            // Draw item text
            let center_y = rc_item.y + rc_item.h / 2
            ctx.fillStyle = globalStyle.crText
            ctx.textBaseline = 'middle'
            if (this.optItemAlign == LIST_ITEM_LEFT){
                ctx.textAlign = 'left'
                ctx.fillText(this.lstContent[index], rc_item.x, center_y)
            }
            else{//LIST_ITEM_CENTER
                let center_x = rc_item.x + rc_item.w / 2
                ctx.textAlign = 'center'
                ctx.fillText(this.lstContent[index], center_x, center_y)
            }
        }

        this.DrawFrame(ctx, globalStyle.numFrameBrdWidth, globalStyle.crFrameBorder)

        if (this.v_scolbar){
            this.v_scolbar.Draw(ctx)
        }
    }

    IsSelected(index){
        for (let i = 0; i < this.lstSelected.length; i++){
            if (this.lstSelected[i] == index){return true}
        }
        return false
    }

    DoSelect(index){
        if (! this.IsSelected(index)){
            if (this.optItemStyle == LIST_ITEM_SINGEL_CHECK){
                this.DoClrSel()
            }
            this.lstSelected.push(index)
            this.OnSelChange()
        }
    }

    DoUnselect(index){
        if (this.IsSelected(index)){
            for (let i = 0; i < this.lstSelected.length; i++){
                if (this.lstSelected[i] == index){
                    this.lstSelected.splice(i, 1)
                    break
                }
            }
            this.OnSelChange()
        }
    }

    DoClrSel(){
        this.lstSelected.splice(0)
    }

    DoSetTopIndex(index){
        this.numTopIndex = index
        if (this.numTopIndex + this.numCanDisplayed > this.lstContent.length){
            this.numTopIndex = this.lstContent.length - this.numCanDisplayed
        }
        if (this.numTopIndex < 0){
            this.numTopIndex = 0
        }
        if (this.v_scolbar){
            this.v_scolbar.DoSetPos(this.numTopIndex)
        }
        this.PushPaintTask(this)
    }

    OnSelChange(){} // Overload this function

    OnClickItem(index){ // Overload this function
        if (DEBUG){alert ("The index you click is: " + index)}
    }
}

class CQCombo extends CQWnd{
    constructor(cq, layout, item_list, cur_sel, list_cap){
        super(cq, layout.rc)
        layout.SetBoundWnd(this)
        this.r = globalStyle.numRoundCorner
        this.title = ""
		this.numCurSel = cur_sel
        this.numListCap = list_cap
        this.lstContent = item_list.concat()
        if (cur_sel >= 0){
            this.title = item_list[cur_sel]
        }
        this.isPopDown = false
    }

    DrawSelf(ctx){
        this.DrawButtonBase(ctx)

        let rc_text = new Rect(
            this.rc.x + this.r,
            this.rc.y,
            this.rc.w - (this.r * 2) - globalStyle.numCheckMarkOccupy,
            this.rc.h)
        let rcPopBtn = new Rect(
            this.rc.right() - this.r - globalStyle.numCheckMarkOccupy,
            this.rc.y,
            globalStyle.numCheckMarkOccupy,
            this.rc.h)
        
        CQWnd.DrawTextCenter(ctx, rc_text, this.title)
        CQWnd.DrawPopButton(ctx, rcPopBtn)
    }

    MsgProc(msg, param){
        if (super.MsgProc(msg, param)){return true}

        switch (msg){
        case MSG_MOUSE_CLICK:
            if (this.rc.PtInRect(param)){
                this.DoListPopDown()
                return true
            }
        }

        return false
    }

    SetContent(item_list){
        if (this.isPopDown){
            this.DoListPackUp()
        }
        this.numCurSel = -1
        this.lstContent.splice(0)
        this.lstContent = item_list.concat()
    }

    DoSelect(index){
        if (index == this.numCurSel){return}
        
        this.numCurSel = index
        if (index >= 0){
            this.title = this.lstContent[index]
        }
        this.PushPaintTask(this)
        this.OnChange(index)
    }

    DoClrSel(){
        this.DoSelect(-1)
    }

    DoListPopDown(){
        if (this.wndPopList){return}

        // The height of the drop-down list, in terms of the number of entries
        let list_cap = Math.min(this.numListCap, this.lstContent.length)
        let rc_pop = new Rect(
            this.rc.x - globalStyle.numBoxMarginWidth,
            this.rc.bottom(),
            this.rc.w + globalStyle.numBoxMarginWidth * 2,
            list_cap * globalStyle.numListItemHeight + (globalStyle.numFrameBrdWidth + globalStyle.numBoxMarginWidth) * 2)
        if (rc_pop.x < 0){rc_pop.x = 0}
        if (rc_pop.y < 0){rc_pop.y = 0}
        if (rc_pop.x + rc_pop.w > this.cq.ctx_w){rc_pop.x = this.cq.ctx_w - rc_pop.w}
        if (rc_pop.y + rc_pop.h > this.cq.ctx_h){rc_pop.y = this.cq.ctx_h - rc_pop.h}

        this.wndPopBox = this.cq.AddIndependentBox(rc_pop, "", BOX_CLOSE_OUTER_CLICK)
        this.wndPopBox.inner_layout.rc = rc_pop.Clone()
        this.wndPopBox.inner_layout.rc.Shrink(
            globalStyle.numBoxMarginWidth,
            globalStyle.numBoxMarginWidth,
            globalStyle.numBoxMarginWidth,
            globalStyle.numBoxMarginWidth)
        this.wndPopBox.DrawSelf = function(ctx){
            CQWnd.DrawUpperFrame(ctx, this.rc, this.r)
        }

        let pop_list = this.wndPopBox.AddList(this.wndPopBox.inner_layout, LIST_ITEM_CLICK_MSG, this.lstContent)
        pop_list.wndBoundCombo = this
        pop_list.DoSetTopIndex(this.numCurSel >= 0 ? this.numCurSel : 0)
        pop_list.OnClickItem = function(index){
            this.wndBoundCombo.DoSelect(index)
            this.wndBoundCombo.DoListPackUp()
        }

        this.isPopDown = true
        this.OnListPopDown()
    }

    DoListPackUp(){
        if (! this.wndPopBox){return}

        this.cq.CloseIndependentBox(this.wndPopBox)
        this.isPopDown = false
        this.OnListPackUp()
    }

    MouseStatusChange(){
        this.PushPaintTask(this)
    }

    OnChange(index){} // Overload this function

    OnListPopDown(){} // Overload this function

    OnListPackUp(){} // Overload this function
}

class CQPrgsBar extends CQWnd{
    constructor(cq, layout, format, percentage){
        super(cq, layout.rc)
        layout.SetBoundWnd(this)
        this.r = 0
        this.optTitleAlign = globalStyle.numPrgsTitleAlign
        this.txtTitleFormat = format
        this.numCurPrgs = percentage

        switch (this.optTitleAlign){
        case PRGSBAR_TITLE_CENTER:
            this.rcText = this.rc.Clone()
            this.rcBarBk = new Rect(
                this.rc.x,
                this.rc.y + Math.floor((this.rc.h - globalStyle.numScolBarFrameHeight) / 2),
                this.rc.w,
                globalStyle.numScolBarFrameHeight)
            break
        case PRGSBAR_TITLE_UPSIDE:
            this.rcText = new Rect(
                this.rc.x,
                this.rc.y,
                this.rc.w,
                this.rc.h - globalStyle.numScolBarFrameHeight)
            this.rcBarBk = new Rect(
                this.rc.x,
                this.rc.bottom() - globalStyle.numScolBarFrameHeight,
                this.rc.w,
                globalStyle.numScolBarFrameHeight)
            break
        case PRGSBAR_TITLE_BELOW:
            this.rcText = new Rect(
                this.rc.x,
                this.rc.y + globalStyle.numScolBarFrameHeight,
                this.rc.w,
                this.rc.h - globalStyle.numScolBarFrameHeight)
            this.rcBarBk = new Rect(
                this.rc.x,
                this.rc.y,
                this.rc.w,
                globalStyle.numScolBarFrameHeight)
            break
        }

        let brd = globalStyle.numScolBarBrdWidth
        this.rcBar = this.rcBarBk.Clone()
        this.rcBar.Shrink(brd, brd, brd, brd)
        this.r_BarBk = Math.floor(globalStyle.numScolBarFrameHeight / 2)
        this.r_Bar = this.r_BarBk - brd
    }

    MsgProc(msg, param){
        super.MsgProc(msg, param)
    }

    DrawSelf(ctx){
        ctx.fillStyle = globalStyle.crFrameFloor
        ctx.fillRect(this.rc.x, this.rc.y, this.rc.w, this.rc.h)

        CQWnd.DrawBaseFrame(ctx, this.rcBarBk, this.r_BarBk)

        let rc_upper_btn = this.GetUpperBtnRect()
        if (rc_upper_btn){CQWnd.DrawUpperFrame(ctx, rc_upper_btn, this.r_Bar)}

        let txt = this.txtTitleFormat.replace("|", "" + this.numCurPrgs)
        CQWnd.DrawTextCenter(ctx, this.rcText, txt)
    }

    GetUpperBtnRect(){
        if (this.numCurPrgs < 1){return null}

        let btn_w = Math.floor(this.rcBar.w * this.numCurPrgs / 100)
        if (btn_w < this.rcBar.h){ // The smallest button is also square, it can’t be smaller
            btn_w = this.rcBar.h
        }
        return new Rect(this.rcBar.x, this.rcBar.y, btn_w, this.rcBar.h)
    }

    DoSetPrgs(prgs){
        if (this.numCurPrgs == prgs){return}

        this.numCurPrgs = Math.floor(prgs)
        if (this.numCurPrgs < 0){
            this.numCurPrgs = 0
        }
        if (this.numCurPrgs > 100){
            this.numCurPrgs = 100
        }
        this.PushPaintTask(this)
        this.OnPrgsChange(this.numCurPrgs)
    }

    OnPrgsChange(prgs){} // Overload this function
}

class CQLabel extends CQWnd{
    constructor(cq, layout, type, txt_or_img, align, color_or_imgSize){
        super(cq, layout.rc)
        layout.SetBoundWnd(this)
        this.r = 0
        this.optType = type
        this.optAlign = align
        if (type == LABEL_TYPE_TEXT){
            this.txt = txt_or_img
            this.crText = color_or_imgSize
        }
        else{
            this.img = txt_or_img
            this.szImg = color_or_imgSize
        }
    }

    MsgProc(msg, param){
        if (super.MsgProc(msg, param)){return true}

        switch (msg){
        case MSG_MOUSE_CLICK:
            if (this.rc.PtInRect(param)){
                this.OnClick()
                return true
            }
        }

        return false
    }

    DrawSelf(ctx){
        switch (this.optType){
        case LABEL_TYPE_TEXT:
            let center_y = this.rc.y + this.rc.h / 2
            ctx.fillStyle = this.crText
            ctx.textBaseline = 'middle'
            if (this.optAlign == LABEL_TXT_LEFT){
                ctx.textAlign = 'left'
                ctx.fillText(this.txt, this.rc.x, center_y)
            }
            else{//LABEL_TXT_CENTER
                let center_x = this.rc.x + this.rc.w / 2
                ctx.textAlign = 'center'
                ctx.fillText(this.txt, center_x, center_y)
            }
            break
        case LABEL_TYPE_IMAGE:
            // drawImage parameter list (image,sx,sy,sWidth,sHeight,x,y,width,height)
            if (this.optAlign == LABEL_IMG_CENTERED){
                let margin_lft_rht = Math.floor((this.rc.w - this.szImg.w) / 2)
                let margin_top_btm = Math.floor((this.rc.h - this.szImg.h) / 2)
                let sx = 0
                let sy = 0
                let sw = this.szImg.w
                let sh = this.szImg.h
                if (margin_lft_rht < 0){
                    sx -= margin_lft_rht // Note that margin_lft_rht is negative!
                    margin_lft_rht = 0
                    sw = this.rc.w
                }
                if (margin_top_btm < 0){
                    sy -= margin_top_btm // Note that margin_top_btm is negative!
                    margin_top_btm = 0
                    sh = this.rc.h
                }
                ctx.drawImage(this.img, sx, sy, sw, sh,
                              this.rc.x + margin_lft_rht,
                              this.rc.y + margin_top_btm, sw, sh)
            }
            else{//LABEL_IMG_STRETCHED
                ctx.drawImage(this.img, 0, 0, this.szImg.w, this.szImg.h,
                              this.rc.x, this.rc.y, this.rc.w, this.rc.h)
            }
            break
        }
    }

    OnClick(){} // Overload this function
}

class CQMsg{
    constructor(wnd, msg, param){
        this.wnd = wnd
        this.msg = msg
        this.param = param
    }

    static IsMouseMsg(msg){
        return (msg == MSG_MOUSE_DOWN
             || msg == MSG_MOUSE_UP
             || msg == MSG_MOUSE_MOVE
             || msg == MSG_MOUSE_CLICK
             || msg == MSG_MOUSE_WHEEL)
    }
}

class CQPaintQueue{
    constructor(){
        this.queue = new Array
    }

    GetTask(){
        return this.queue.shift()
    }

    PushTask(in_wnd, in_alpha){
        // Check existing drawing tasks and merge them if there are related tasks
        for(let i = 0; i < this.queue.length; i++){
            if(this.queue[i].wnd.CheckProgenitor(in_wnd)){
                this.queue[i] = {wnd: in_wnd, alpha: in_alpha}
                return
            }
            if(in_wnd.CheckProgenitor(this.queue[i].wnd)){
                // If the Wnd in the queue is the parent of the new Wnd, there is no need to draw
                // Because the parent Wnd will recursively call the drawing functions of all child Wnd when drawing
                return
            }
        }
        this.queue.push({wnd: in_wnd, alpha: in_alpha})
    }

    Clear(){
        this.queue.splice(0)
    }
}

class CQTimer{
    constructor(wnd, id, interval){
        this.wnd = wnd
        this.id = id
        this.msInterval = interval
        this.msCounter = 0
    }
}

class CQGUI{
    constructor(canvas){
        if(!CQGUI.instance){
            let ctx = canvas.getContext("2d")
            let w = canvas.width
            let h = canvas.height
            CQGUI.instance = this
            CQGUI.instance.canvas = canvas
            CQGUI.instance.ctx = ctx
            CQGUI.instance.ctx_w = w
            CQGUI.instance.ctx_h = h
            CQGUI.instance.ctx.font = globalStyle.font
            CQGUI.instance.msg_queue = new Array
            CQGUI.instance.paint_queue = new CQPaintQueue
            CQGUI.instance.timerQueue = new Array
            CQGUI.instance.lstBoxStack = new Array // Only the CQBox at the top of the stack handles the message
            CQGUI.instance.wndTrackMouse = null //The window that tracks the movement of the mouse,
                                                // and mouse messages are sent to this window first for processing
        }
        return CQGUI.instance
    }

    static getInstance(canvas){
        if(!this.instance){
            this.instance = new CQGUI(canvas)
        }
        return this.instance
    }

    static init(canvas){
        return CQGUI.getInstance(canvas)
    }

    LoadStyle(json){
        try{
            globalStyle.load(json)
        }
        catch(err){
            if(DEBUG){
                alert(err.description)
                throw err.description
            }
        }
    }

    Start(){
        // Start the event loop
        setInterval(function(){

            let cq = CQGUI.instance
            // Check the timers
            cq._InspectTimerQueue()
            // Processing event queue
            while(1){
                let cq_msg = cq.msg_queue.shift()
                if (!cq_msg) break

                try{
                    let wnd = cq_msg.wnd
                    wnd.MsgProc(cq_msg.msg, cq_msg.param)
                }
                catch(err){
                    if(DEBUG){
                        alert(err.description)
                        throw err.description
                    }
                }
            }// end while

            // Handling redraw events
            while(1){
                let task = cq.paint_queue.GetTask()
                if (!task) break

                try{
                    cq.ctx.globalAlpha = task.alpha
                    task.wnd.Draw(cq.ctx)
                }
                catch(err){
                    if(DEBUG){
                        alert(err.description)
                        throw err.description
                    }
                }
            }// end while
        }, CQGUI_TICK)

        // Set the mouse event handler functions
        this.canvas.onmousedown = function(e){
            CQGUI.instance.PushMouseMsg(e, MSG_MOUSE_DOWN)
            e.stopPropagation()
        }

        this.canvas.onmouseup = function(e){
            CQGUI.instance.PushMouseMsg(e, MSG_MOUSE_UP)
            e.stopPropagation()
        }

        this.canvas.onmousemove = function(e){
            CQGUI.instance.PushMouseMsg(e, MSG_MOUSE_MOVE)
            e.stopPropagation()
        }

        this.canvas.onclick = function(e){
            CQGUI.instance.PushMouseMsg(e, MSG_MOUSE_CLICK)
            e.stopPropagation()
        }

        this.canvas.onwheel = function(e){
            CQGUI.instance.PushMouseMsg(e, MSG_MOUSE_WHEEL)
            e.stopPropagation()
        }
    }

    PushMouseMsg(e, msg){

        // Get relative coordinates
        let x = e.pageX
        let y = e.pageY
        let canvas = e.target
        let bbox = canvas.getBoundingClientRect()
		let pt = {x: x - bbox.left * (canvas.width / bbox.width),
			      y: y - bbox.top * (canvas.height / bbox.height),
                  deltaY: e.deltaY}

        // Mouse messages are sent first to the window tracking the mouse
        if (this.wndTrackMouse && CQMsg.IsMouseMsg(msg)){
            this.PushMsg(this.wndTrackMouse, msg, pt)
            return
        }

        // Independent dialog box monopolizes all messages
        if (this.lstBoxStack.length > 0){
            let box = this.lstBoxStack[this.lstBoxStack.length - 1]
            this.PushMsg(box, msg, pt)
            return
        }

        // Check the scope of which Layout the mouse is in? The target window is set to the window bound to this layout
        // If the binding window of the Layout is null, it means that the mouse is in a blank area and no message is required.
        let target = this.root_layout.GetTargetLayout(pt)
        if (target && target.wndBound){
            this.PushMsg(target.wndBound, msg, pt)
        }
    }

    PushMsg(wnd, msg, param){
        this.msg_queue.push(new CQMsg(wnd, msg, param))
    }

    PushPaintTask(wnd, alpha = 1.0){
        this.paint_queue.PushTask(wnd, alpha)
    }

    SetTimer(wnd, id, interval){
        // Check the existing timer, if Wnd and Id are the same, only modify the parameters
        for(let i = 0; i < this.timerQueue.length; i++){
            if (this.timerQueue[i].wnd == wnd && this.timerQueue[i].id == id){
                this.timerQueue[i].msInterval = interval
                this.timerQueue[i].msCounter = 0
                return
            }
        }
        this.timerQueue.push(new CQTimer(wnd, id, interval))
    }

    KillTimer(wnd, id){
        for(let i = 0; i < this.timerQueue.length; i++){
            if (this.timerQueue[i].wnd == wnd && this.timerQueue[i].id == id){
                this.timerQueue.splice(i,1)
                return
            }
        }
    }

    _InspectTimerQueue(){
        for(let i = 0; i < this.timerQueue.length; i++){
            this.timerQueue[i].msCounter += CQGUI_TICK
            if (this.timerQueue[i].msCounter >= this.timerQueue[i].msInterval){
                this.timerQueue[i].msCounter -= this.timerQueue[i].msInterval
                this.PushMsg(
                    this.timerQueue[i].wnd,
                    MSG_TIMER,
                    this.timerQueue[i].id)
            }
        }
    }

    GetRootLayout(root_layout_type){
        if (! this.root_layout){
            // The root layout should be filled with the entire canvas
            let layout = new CQLayout(null, root_layout_type, LAYOUT_STG_UNSPECIFIED, 0)
            layout.rc = new Rect(0, 0, this.ctx_w, this.ctx_h)
            layout.rcEmpty = layout.rc.Clone()
            this.root_layout = layout
        }
        return this.root_layout
    }

    AddLayout(layout, type, stg, apt_sz){
        return new CQLayout(layout, type, stg, apt_sz)
    }

    AddWindow(layout){
        let wnd = new CQWnd(this, layout.rc)
        layout.SetBoundWnd(wnd)
        return wnd
    }

    AddBoxOnLayout(layout, title){
        // In principle, this type of Dialog Box does not need to be closed
        let box = new CQBox(this, layout.rc, title, BOX_CLOSE_PASSIVE)
        layout.SetBoundWnd(box)
        return box
    }

    GetCenterRect(w, h, offset_x = 0, offset_y = 0){
        return new Rect(Math.floor((this.ctx_w - w) / 2) + offset_x,
                        Math.floor((this.ctx_h - h) / 2) + offset_y,
                        w, h)
    }
    AddIndependentBox(rc, title, close_type = globalStyle.numBoxCloseStyle){
        let box = new CQBox(this, rc, title, close_type)
        this.lstBoxStack.push(box)
        this.paint_queue.Clear()
        this.msg_queue.splice(0)
        this.wndTrackMouse = null
        this.DrawAll()
        return box
    }

    CloseIndependentBox(box){
        for(let i = 0; i < this.lstBoxStack.length; i++){
            if (this.lstBoxStack[i] == box){
                // The Wnds behind this Box are stacked on the top of this box, all closed
                let lstUpperBox = this.lstBoxStack.splice(i)
                if (lstUpperBox.length > 0){
                    for (let j = (lstUpperBox.length - 1); j >= 0; j--){
                        lstUpperBox[j].OnCloseBox()
                    }
                }
                this.wndTrackMouse = null
                this.paint_queue.Clear()
                this.msg_queue.splice(0)
                this.DrawAll()
                return
            }
        }
    }

    AddButton(layout, title){
        return new CQButton(this, layout, title)
    }

    AddCheck(layout, title, checked){
        return new CQCheck(this, layout, title, checked)
    }

    AddSpin(layout, value, digit, step){
        return new CQSpin(this, layout, value, digit, step)
    }

    AddList(layout, item_style, item_list){
        return new CQList(this, layout, item_style, item_list)
    }

    AddCombo(layout, item_list, cur_sel, list_cap){
        return new CQCombo(this, layout, item_list, cur_sel, list_cap)
    }

    AddPrgsBar(layout, format, percentage){
        return new CQPrgsBar(this, layout, format, percentage)
    }

    AddTxtLabel(layout, txt, align, color){
        return new CQLabel(this, layout, LABEL_TYPE_TEXT, txt, align, color)
    }

    AddImgLabel(layout, img, align, size){
        return new CQLabel(this, layout, LABEL_TYPE_IMAGE, img, align, size)
    }

    DrawAll(){
        // Draw the background part
        this.ctx.globalAlpha = 1.0
        this.root_layout.DrawBoundWnd(this.ctx)
        let box_count = this.lstBoxStack.length
        if (box_count == 0) {return}

        let wnd_last_draw = this.lstBoxStack[box_count-1]

        // Non-top-level windows are drawn semi-transparently
        for(let i = 0; i < box_count; i++){
            if (this.lstBoxStack[i] != wnd_last_draw){
                this.PushPaintTask(this.lstBoxStack[i], globalStyle.numAlphaOfRearBox)
            }
        }

        // The top-level window is drawn in an opaque manner
        this.PushPaintTask(wnd_last_draw)
    }
}
